package iblink.adapter.request;

import java.util.Set;

import iblink.adapter.IBClient;
import iblink.adapter.IBWrapper;
import iblink.event.IBErrorEvent;
import iblink.event.IBErrorEvent.IbErrorType;
import iblink.exception.IBErrorEventException;
import iblink.exception.IBLinkException;
import packutil.ThreadUtil;
import packutil.event.IEvent;
import packutil.event.IEventListener;
import packutil.exception.TimeoutException;
import packutil.idgen.IIdGenerator;

public abstract class ARequest implements IRequest, IEventListener {

	private static String ERR_INVALID_REQ_ID = "Invalid request ID";
	private static int REQUEST_WAIT_TIMEOUT_MILLIS = 10000;

	public class WaitObject {
		public boolean activated = false;
	}

	private final IBClient ibClient;
	private final WaitObject waitObject = new WaitObject();
	private final IBWrapper ibWrapper;
	private final IIdGenerator requestIdGenerator;

	private int requestId;
	private String errorMsg = null;
	private IEvent response = null;
	protected boolean executed = false;

	public ARequest(IBClient ibClient, IIdGenerator requestIdGenerator) {
		this.requestIdGenerator = requestIdGenerator;
		this.ibClient = ibClient;
		this.ibWrapper = ibClient.wrapper();
	}

	public void execute() {
		requestId = requestIdGenerator.nextIdAsInt();
		this.ibWrapper.addEventListener(this, requestId);
		execute(ibClient, requestId);
	}

	protected abstract void execute(IBClient cli, int requestId);

	@Override
	public void onEvent(IEvent event) {

		if (event.getEventId() != requestId) {
			errorMsg = ERR_INVALID_REQ_ID;
		}

		this.response = event;

		synchronized (waitObject) {
			waitObject.activated = true;
			waitObject.notify();
		}
	}

	protected IEvent waitForResponse() {
		return waitForResponse(null);
	}

	/**
	 * Wait for response event
	 * 
	 * @param requestId
	 *            request id to wait on
	 * @param errorExemptions
	 *            If the response event is an error event, an exception will be
	 *            thrown UNLESS it matches one of these exceptions. If it does,
	 *            the error event will be returned to the requester
	 * @return response event
	 */
	protected IEvent waitForResponse(Set<IbErrorType> errorExemptions) {

		// wait for thread to be awakened by response
		// while loop to prevent spurious wakeup (see javadocs for Thread)
		long startTime = System.currentTimeMillis();
		while (!waitObject.activated) {
			ThreadUtil.wait(waitObject, REQUEST_WAIT_TIMEOUT_MILLIS);

			if (System.currentTimeMillis() - startTime > REQUEST_WAIT_TIMEOUT_MILLIS)
				throw new TimeoutException("Waited for response from IB for "
						+ REQUEST_WAIT_TIMEOUT_MILLIS
						+ " millis, but it never came..");
		}

		if (errorMsg != null) {

			if (errorMsg.equals(ERR_INVALID_REQ_ID)) {
				throw new IBLinkException(
						"Fatal error: Request made and response was delivered with a different request ID! Got to be a bug");
			}

			throw new IBLinkException("Error: " + errorMsg);
		}

		if (response == null) {
			throw new IBLinkException(
					"Fatal error. Thread was awakened but response isn't there!");
		}

		// if its an error event and not exempted, throw it up
		if (errorExemptions != null
				&& response instanceof IBErrorEvent
				&& !errorExemptions.contains(((IBErrorEvent) response)
						.getErrorType())) {
			throw new IBErrorEventException((IBErrorEvent) response);
		}

		executed = true;
		return response;
	}
}